﻿using Remotely.Server.Hubs;
using Remotely.Shared.Entities;
using Remotely.Shared.Enums;
using Remotely.Shared.Utilities;

namespace Remotely.Server.Services;

public interface IScriptScheduleDispatcher
{
    Task DispatchPendingScriptRuns();
}

public class ScriptScheduleDispatcher : IScriptScheduleDispatcher
{
    private readonly IDataService _dataService;
    private readonly IAgentHubSessionCache _serviceSessionCache;
    private readonly ICircuitConnection _circuitConnection;
    private readonly ILogger<ScriptScheduleDispatcher> _logger;

    public ScriptScheduleDispatcher(IDataService dataService,
        IAgentHubSessionCache serviceSessionCache,
        ICircuitConnection circuitConnection,
        ILogger<ScriptScheduleDispatcher> logger)
    {
        _dataService = dataService;
        _serviceSessionCache = serviceSessionCache;
        _circuitConnection = circuitConnection;
        _logger = logger;
    }

    public async Task DispatchPendingScriptRuns()
    {
        try
        {
            _logger.LogDebug("Script Schedule Dispatcher started.");

            var schedules = await _dataService.GetScriptSchedulesDue();

            if (schedules?.Any() != true)
            {
                _logger.LogDebug("No schedules are due.");
                return;
            }

            foreach (var schedule in schedules)
            {
                try
                {
                    _logger.LogDebug("Considering {scheduleName}.  Interval: {interval}. Next Run: {nextRun}.",
                        schedule.Name,
                        schedule.Interval,
                        schedule.NextRun);

                    if (!AdvanceSchedule(schedule))
                    {
                        _logger.LogDebug("Schedule is not due.");
                        continue;
                    }

                    _logger.LogDebug("Creating script run for schedule {scheduleName}.", schedule.Name);

                    var scriptRun = new ScriptRun()
                    {
                        OrganizationID = schedule.OrganizationID,
                        RunAt = Time.Now,
                        SavedScriptId = schedule.SavedScriptId,
                        RunOnNextConnect = schedule.RunOnNextConnect,
                        Initiator = $"Schedule: {schedule.Name}",
                        ScheduleId = schedule.Id,
                        InputType = ScriptInputType.ScheduledScript

                    };

                    var deviceIdsFromDeviceGroups = schedule.DeviceGroups?.SelectMany(dg => 
                        dg.Devices.Select(d => d.ID));

                    var deviceIds = schedule.Devices.Select(x => x.ID)
                        .Concat(deviceIdsFromDeviceGroups ?? Array.Empty<string>())
                        .Distinct()
                        .ToArray();

                    var onlineDevices = _serviceSessionCache.FilterDevicesByOnlineStatus(deviceIds, true);

                    if (schedule.RunOnNextConnect)
                    {
                        scriptRun.Devices = _dataService.GetDevices(deviceIds);
                    }
                    else
                    {
                        scriptRun.Devices = _dataService.GetDevices(onlineDevices);
                    }

                    await _dataService.AddScriptRun(scriptRun);

                    await _circuitConnection.RunScript(onlineDevices, schedule.SavedScriptId, scriptRun.Id, ScriptInputType.ScheduledScript, true);

                    _logger.LogDebug("Created script run for schedule {scheduleName}.", schedule.Name);

                    schedule.LastRun = Time.Now;
                    await _dataService.AddOrUpdateScriptSchedule(schedule);

                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error while generating script run.");
                }
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error while dispatching script runs.");
        }
    }

    private bool AdvanceSchedule(ScriptSchedule schedule)
    {
        if (schedule is null)
        {
            return false;
        }

        if (schedule.NextRun > Time.Now)
        {
            return false;
        }

        switch (schedule.Interval)
        {
            case RepeatInterval.Hourly:
                CatchUpNextRun(schedule, TimeSpan.FromHours(1));
                break;
            case RepeatInterval.Daily:
                CatchUpNextRun(schedule, TimeSpan.FromDays(1));
                break;
            case RepeatInterval.Weekly:
                CatchUpNextRun(schedule, TimeSpan.FromDays(7));
                break;
            case RepeatInterval.Monthly:
                for (var i = 0; schedule.NextRun < Time.Now; i++)
                {
                    schedule.NextRun = schedule.StartAt.AddMonths(i);
                }
                break;
            default:
                return false;
        }

        return true;
    }


    private void CatchUpNextRun(ScriptSchedule schedule, TimeSpan interval)
    {
        while (schedule.NextRun < Time.Now)
        {
            schedule.NextRun += interval;
        }
    }
}
